use crate::error::Error;
use proc_macro2::TokenStream;
use quote::quote;
use syn::Attribute;
use syn::DataEnum;
use syn::DataStruct;
use syn::DataUnion;
use syn::DeriveInput;
use syn::Generics;
use syn::Ident;
use syn::Result;
use syn::Visibility;

pub struct InputStructRef<'a> {
    pub vis: &'a Visibility,
    pub attrs: &'a Vec<Attribute>,
    pub ident: &'a Ident,
    pub generics: &'a Generics,
    pub data_struct: &'a DataStruct,
}

pub trait GetImplTraitToken {
    fn get_impl_trait_token(
        &self,
        trait_token: TokenStream,
        for_ident: &Ident,
        body_token: TokenStream,
    ) -> TokenStream;
}

impl GetImplTraitToken for &Generics {
    fn get_impl_trait_token(
        &self,
        trait_token: TokenStream,
        for_ident: &Ident,
        body_token: TokenStream,
    ) -> TokenStream {
        let (impl_generics, ty_generics, where_clause) = self.split_for_impl();
        quote! {
            impl #impl_generics #trait_token for #for_ident #ty_generics #where_clause {
                #body_token
            }
        }
    }
}

impl<'a> InputStructRef<'a> {
    pub fn new(input: &'a DeriveInput, data_struct: &'a DataStruct) -> Self {
        Self {
            vis: &input.vis,
            attrs: &input.attrs,
            ident: &input.ident,
            generics: &input.generics,
            data_struct,
        }
    }
}

pub struct InputEnumRef<'a> {
    pub vis: &'a Visibility,
    pub attrs: &'a Vec<Attribute>,
    pub ident: &'a Ident,
    pub generics: &'a Generics,
    pub data_enum: &'a DataEnum,
}

impl<'a> InputEnumRef<'a> {
    pub fn new(input: &'a DeriveInput, data_enum: &'a DataEnum) -> Self {
        Self {
            vis: &input.vis,
            attrs: &input.attrs,
            ident: &input.ident,
            generics: &input.generics,
            data_enum,
        }
    }
}

pub struct InputUnionRef<'a> {
    pub vis: &'a Visibility,
    pub attrs: &'a Vec<Attribute>,
    pub ident: &'a Ident,
    pub generics: &'a Generics,
    pub data_union: &'a DataUnion,
}

impl<'a> InputUnionRef<'a> {
    pub fn new(input: &'a DeriveInput, data_union: &'a DataUnion) -> Self {
        Self {
            vis: &input.vis,
            attrs: &input.attrs,
            ident: &input.ident,
            generics: &input.generics,
            data_union,
        }
    }
}

pub trait FromDeriveInput<'a>: Sized {
    fn from_derive_input(input: &'a DeriveInput) -> Result<Self> {
        match &input.data {
            syn::Data::Struct(data_struct) => {
                Self::from_data_struct(InputStructRef::new(input, data_struct))
            }
            syn::Data::Enum(data_enum) => Self::from_data_enum(InputEnumRef::new(input, data_enum)),
            syn::Data::Union(data_union) => {
                Self::from_data_union(InputUnionRef::new(input, data_union))
            }
        }
    }

    #[allow(unused_variables)]
    fn from_data_struct(input: InputStructRef<'a>) -> Result<Self> {
        Err(Error::unexpected_type("struct").into())
    }

    #[allow(unused_variables)]
    fn from_data_enum(input: InputEnumRef<'a>) -> Result<Self> {
        Err(Error::unexpected_type("enum").into())
    }

    #[allow(unused_variables)]
    fn from_data_union(input: InputUnionRef<'a>) -> Result<Self> {
        Err(Error::unexpected_type("union").into())
    }
}
